/*******************************************************************************
 * Copyright (c) 2010-2011 Robert Fraser                                       *
 *                                                                             *
 * Permission is hereby granted, free of charge, to any person obtaining a     *
 * copy of this software and associated documentation files (the "Software"),  *
 * to deal in the Software without restriction, including without limitation   *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,    *
 * and/or sell copies of the Software, and to permit persons to whom the       *
 * Software is furnished to do so, subject to the following conditions:        *
 *                                                                             *
 *   - The above copyright notice and this permission notice shall be included *
 *     in all copies or substantial portions of the Software.                  *
 *                                                                             *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER      *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING     *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER         *
 * DEALINGS IN THE SOFTWARE.                                                   *
 *******************************************************************************/

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Media;
using zeal.ui.util;
using zeal.util;

namespace zeal.ui.graph
{
    // Responsible for the positioning/sizing of the underlying GraphSurface via render transforms, and
    // animation thereof. Works sort of like a scroll viewer, except not.
    [TemplatePart(Name = "PART_horizontal", Type = typeof(ScrollBar))]
    [TemplatePart(Name = "PART_vertical", Type = typeof(ScrollBar))]
    internal sealed partial class GraphViewer : ContentControl
    {
        private const double SURFACE_MARGIN = GraphEditor.GRID_SIZE * 2;
        private const double SCROLL_BAR_SMALL_CHANGE = GraphEditor.GRID_SIZE;
        private const double SCROLL_BAR_LARGE_CHANGE = SCROLL_BAR_SMALL_CHANGE * 5;
        private const double TOOLTIP_OFFSET = 15;
        
        private readonly MeasuringCanvas _surface;
        private readonly Panel _overlay;
        private readonly ViewerPanel _panel;

        public MeasuringCanvas surface { get { return _surface; }}
        public Panel overlay { get { return _overlay; }}

        public GraphViewer()
        {
            _surface = new MeasuringCanvas();
            _overlay = new Canvas();

            // Setup the translation
            TranslateTransform transform = new TranslateTransform();
            Bindings.set(transform, TranslateTransform.XProperty.of<double>(), this, OriginXProperty, x => -x);
            Bindings.set(transform, TranslateTransform.YProperty.of<double>(), this, OriginYProperty, y => -y);
            _surface.RenderTransform = transform;
            _overlay.RenderTransform = transform;
            
            // Set the background and make sure it transforms too. Freeze what we can for performance reasons
            DrawingBrush brush = AssemblyResources.cloneApplicationResource<DrawingBrush>("GraphBackgroundBrush");
            brush.Transform = transform;
            brush.RelativeTransform.Freeze();
            brush.Drawing.Freeze();
            
            _panel = new ViewerPanel(_surface, _overlay);
            _panel.Background = brush;
            Panel.SetZIndex(_surface, 0);
            Panel.SetZIndex(_overlay, 1);
            _panel.Children.Add(_surface);
            _panel.Children.Add(_overlay);
            Content = _panel;
            ClipToBounds = true;
        }

        public override void OnApplyTemplate()
        {
            setScrollBarBindings(this.getTemplatePart<ScrollBar>("PART_horizontal"), ActualWidthProperty.of<double>(), MeasuringCanvas.MinXProperty, MeasuringCanvas.MaxXProperty, OriginXProperty);
            setScrollBarBindings(this.getTemplatePart<ScrollBar>("PART_vertical"), ActualHeightProperty.of<double>(), MeasuringCanvas.MinYProperty, MeasuringCanvas.MaxYProperty, OriginYProperty);
        }

        /* TODO - Inverted 
        public void translateTo(Rect rect)
        {
            double x = OriginX;
            double y = OriginY;
            Size size = _panel.DesiredSize;
            double left = -x;
            double top = -y;
            double right = left + size.Width;
            double bottom = top + size.Height;

            double dx = 0;
            if (left > rect.Left) dx = left - rect.Left;
            else if (right < rect.Right) dx = right - rect.Right;

            double dy = 0;
            if (top > rect.Top) dy = top - rect.Top;
            else if (bottom < rect.Bottom) dy = bottom - rect.Bottom;

            if (dx == 0 && dy == 0)
                return;

            this.animate(OriginXProperty, x + dx, 0.5, UiUtil.quadraticEase);
            this.animate(OriginYProperty, y + dy, 0.5, UiUtil.quadraticEase);
        } */

        public void pan(double dx, double dy)
        {
            double x = OriginX;
            double y = OriginY;

            OriginX = clamp(x + dx, getOriginMin(_surface.MinX, x), getOriginMax(_surface.MaxX, x, viewortWidth));
            OriginY = clamp(y + dy, getOriginMin(_surface.MinY, y), getOriginMax(_surface.MaxY, y, viewportHeight));
        }

        public void addTooltip(FrameworkElement tooltip, Binding<Point> mousePosition)
        {
            Bindings.set(tooltip, ToolTipPositionProperty, Bindings.multi(mousePosition, Bindings.get(this, OriginXProperty), Bindings.get(this, OriginYProperty), Bindings.get(_panel, ActualWidthProperty.of<double>()), Bindings.get(_panel, ActualHeightProperty.of<double>()), Bindings.get(tooltip, ActualWidthProperty.of<double>()), Bindings.get(tooltip, ActualHeightProperty.of<double>()), getTooltipPosition));
            Bindings.set(tooltip, Canvas.LeftProperty.of<double>(), tooltip, ToolTipPositionProperty, p => p.X);
            Bindings.set(tooltip, Canvas.TopProperty.of<double>(), tooltip, ToolTipPositionProperty, p => p.Y);
            tooltip.IsHitTestVisible = false;
            tooltip.Focusable = false;
            tooltip.IsEnabled = false;
            _overlay.addChild(tooltip);
        }

        public void removeTooltip(FrameworkElement tooltip)
        {
            _overlay.removeChild(tooltip);
            Bindings.clearAll(tooltip);
        }

        private void setScrollBarBindings(ScrollBar scrollBar, DependencyProperty<double> pViewportSize, 
            DependencyProperty<double> pCanvasMin, DependencyProperty<double> pCanvasMax, DependencyProperty<double> pOrigin)
        {
            Binding<double> bViewportSize = Bindings.get(_panel, pViewportSize);
            Binding<double> bOrigin = Bindings.get(this, pOrigin);
            Binding<double> bSurfaceMin = Bindings.get(_surface, pCanvasMin);
            Binding<double> bSurfaceMax = Bindings.get(_surface, pCanvasMax);

            Bindings.set(scrollBar, RangeBase.MaximumProperty.of<double>(), Bindings.multi(bSurfaceMin, bSurfaceMax, bOrigin, bViewportSize, (min, max, x, v) => Math.Max(0, getOriginMax(max, x, v) - getOriginMin(min, x))));
            Bindings.set(scrollBar, RangeBase.ValueProperty.of<double>(), Bindings.multi(bSurfaceMin, Bindings.get(this, pOrigin).with(BindingMode.TwoWay), new ScrollBarValueConverter(this, pCanvasMin, pOrigin)).with(BindingMode.TwoWay));
            Bindings.set(scrollBar, ScrollBar.ViewportSizeProperty.of<double>(), bViewportSize);
            Bindings.set(scrollBar, IsEnabledProperty.of<bool>(), scrollBar, RangeBase.MaximumProperty.of<double>(), x => x > 0);

            scrollBar.SmallChange = SCROLL_BAR_SMALL_CHANGE;
            scrollBar.LargeChange = SCROLL_BAR_LARGE_CHANGE;
        }

        private static Point getTooltipPosition(Point p, double ox, double oy, double vx, double vy, double tx, double ty)
        {
            if(tx == 0 || ty == 0) return new Point(0, 0);

            double x = p.X;
            double y = p.Y;
            Rect screen = new Rect(ox, oy, vx, vy);
            Size size = new Size(tx, ty);

            double x2 = clamp(x, screen.Left, screen.Right - tx);
            Point belowCursor = new Point(x2, y + TOOLTIP_OFFSET);
            Point aboveCursor = new Point(x2, y - TOOLTIP_OFFSET - ty);

            if(screen.Contains(new Rect(belowCursor, size))) return belowCursor;
            if(screen.Contains(new Rect(aboveCursor, size))) return aboveCursor;
            return new Point(x2, clamp(y, screen.Top, screen.Bottom - ty));
        }

        private double viewortWidth { get { return _panel.ActualWidth; } }
        private double viewportHeight { get { return _panel.ActualHeight; } }
        private static double getOriginMin(double s, double x) { return s.isNaN() ? x : Math.Min(s - SURFACE_MARGIN, x); }
        private static double getOriginMax(double s, double x, double v) { return s.isNaN() ? x : (Math.Max(s + SURFACE_MARGIN, x + v) - v); }
        private static double clamp(double v, double min, double max) { return Math.Max(min, Math.Min(max, v)); }

        private sealed class ScrollBarValueConverter : IValueConverter<double, double, double>
        {
            private readonly GraphViewer _this;
            private readonly DependencyProperty<double> _pCanvasMin;
            private readonly DependencyProperty<double> _pOrigin;
            public ScrollBarValueConverter(GraphViewer viewer, DependencyProperty<double> pCanvasMin, DependencyProperty<double> pOrigin)
            {
                _this = viewer;
                _pCanvasMin = pCanvasMin;
                _pOrigin = pOrigin;
            }

            private static readonly object[] _values = new[] { Binding.DoNothing, default(double) };
            object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { return convert((double) values[0], (double) values[1]); }
            object[] IMultiValueConverter.ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { _values[1] = convertBack((double) value); return _values; }

            public double convert(double s, double x) { return x - getOriginMin(s, x); }
            private double convertBack(double x) { return x + getOriginMin(_pCanvasMin.GetValue(_this._surface), _pOrigin.GetValue(_this)); }
        }

        private sealed class ViewerPanel : Panel
        {
            private readonly Panel _surface;
            private readonly Panel _overlay;
            public ViewerPanel(Panel surface, Panel overlay) { _surface = surface; _overlay = overlay; }
            protected override Size MeasureOverride(Size s) { _surface.Measure(UiUtil.INFINITE_SIZE); _overlay.Measure(UiUtil.INFINITE_SIZE); return s; }
            protected override Size ArrangeOverride(Size s) { _surface.Arrange(new Rect(new Point(0, 0), _surface.DesiredSize)); _overlay.Arrange(new Rect(new Point(0, 0), _overlay.DesiredSize)); return s; }
        }
    }
}